// <copyright file="MusicalPart.cs" company="Largo">
// Copyright (c) 2009 All Right Reserved
// </copyright>
// <author> vl </author>
// <email></email>
// <date>2009-01-01</date>
// <summary>Contains ...</summary>

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics.Contracts;
using System.Linq;
using JetBrains.Annotations;
using LargoCommon.Abstract;
using LargoCommon.Interfaces;
using LargoCommon.Localization;

namespace LargoCommon.Music {
    /// <summary>
    /// Musical Part.
    /// </summary>
    public sealed class MusicalPart {
        #region Fields
        /// <summary>
        /// Musical Block.
        /// </summary>
        private MusicalBlock musicalBlock;

        /// <summary>
        /// Musical Track.
        /// </summary>
        private IList<MusicalLine> musicalTracks;

        /// <summary>
        /// Musical Objects.
        /// </summary>
        private Collection<IMusicalLocation> musicalObjects;
        #endregion

        #region Constructors
        /// <summary>
        /// Initializes a new instance of the MusicalPart class.
        /// </summary>
        /// <param name="givenBlock">The given block.</param>
        public MusicalPart(MusicalBlock givenBlock)
        {
            this.MusicalBlock = givenBlock;
            this.MusicalLines = new List<MusicalLine>();
            this.MusicalObjects = new Collection<IMusicalLocation>();
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="MusicalPart"/> class.
        /// </summary>
        [UsedImplicitly]
        public MusicalPart() {         
        }
        #endregion

        #region Public properties
        /// <summary>
        /// Gets Musical Lines.
        /// </summary>
        /// <value> Property description. </value>
        public IList<MusicalLine> MusicalLines {
            get
            {
                Contract.Ensures(Contract.Result<IList<MusicalLine>>() != null);
                return this.musicalTracks ?? (this.musicalTracks = new List<MusicalLine>());
            }
            //// Remove private set - DevExpress
            private set => this.musicalTracks = value;
        }

        /// <summary>
        /// Gets or sets identifier.
        /// </summary>
        /// <value> Property description. </value>
        public string PartId { get; set; }

        /// <summary>
        /// Gets or sets Instrument.
        /// </summary>
        /// <value> Property description. </value>
        public MusicalInstrument Instrument { get; set; }

        /// <summary>
        /// Gets or sets Channel.
        /// </summary>
        /// <value> Property description. </value>
        public MidiChannel Channel { get; set; }

        /// <summary> Gets or sets purpose of the track. </summary>
        /// <value> Property description. </value>
        /// <returns> Returns value. </returns>
        public LinePurpose Purpose { get; set; }   //// CA1044 (FxCop)

        /// <summary>
        /// Gets or sets composed file.
        /// </summary>
        /// <value> Property description. </value>
        /// <summary>
        /// Gets or sets musical block.
        /// </summary>
        /// <value> Property description. </value>
        private MusicalBlock MusicalBlock {
            get {
                Contract.Ensures(Contract.Result<MusicalBlock>() != null);
                if (this.musicalBlock == null) {
                    throw new InvalidOperationException("Musical block is null.");
                }

                return this.musicalBlock;
            }

            set => this.musicalBlock = value ?? throw new ArgumentException(LocalizedMusic.String("Argument cannot be null."), nameof(value));
        }
        #endregion

        #region Private properties
        /// <summary>
        /// Gets or sets Part Number.
        /// </summary>
        /// <value> Property description. </value>
        // ReSharper disable once UnusedAutoPropertyAccessor.Local
        private int PartNumber { [UsedImplicitly] get; set; }

        /// <summary>
        /// Gets or sets Musical Tones.
        /// </summary>
        /// <value> Property description. </value>
        private Collection<IMusicalLocation> MusicalObjects {
            get
            {
                Contract.Ensures(Contract.Result<Collection<IMusicalLocation>>() != null);
                return this.musicalObjects ?? (this.musicalObjects = new Collection<IMusicalLocation>());
            }

            set => this.musicalObjects = value;
        }
        #endregion

        #region Static factory methods
        /// <summary>
        /// Gets new musical track.
        /// </summary>
        /// <param name="musicalBlock">The musical block.</param>
        /// <param name="partNumber">Number of part.</param>
        /// <param name="givenChannel">Midi Channel.</param>
        /// <returns>
        /// Returns value.
        /// </returns>
        public static MusicalPart GetNewMusicalPart(MusicalBlock musicalBlock, int partNumber, MidiChannel givenChannel) {
            var part = GetNewMusicalPart(musicalBlock);
            part.PartNumber = partNumber;
            part.Channel = givenChannel;
            return part;
        }
        #endregion

        #region Public methods
        /// <summary> Add on musical tone to the end of part. </summary>
        /// <param name="musicalObject">Musical object - tone or shift.</param>
        public void AddMusicalObject(IMusicalLocation musicalObject) {
            Contract.Requires(musicalObject != null);
            
            //// if (musicalObject == null) { return; }
            this.MusicalObjects.Add(musicalObject);
        }

        /// <summary>
        /// Moves the objects to staff tracks.
        /// </summary>
        public void MoveObjectsToStaffTracks() {
            //// MusicalTones group by staff and voice
            var trackGroups = (from mt in this.MusicalObjects
                               select new { mt.Staff, mt.InstrumentNumber }).Distinct().ToList(); //// mt.Channel
            this.MusicalLines = new List<MusicalLine>();
            foreach (var tg in trackGroups) {
                var tg1 = tg;
                var voiceObjects = (from mt in this.MusicalObjects
                                    where mt.Staff == tg1.Staff && mt.InstrumentNumber == tg1.InstrumentNumber //// && mt.Channel == tg1.Channel 
                                    orderby mt.BarNumber
                                    select mt).ToList();
                if (!voiceObjects.Any()) {
                    continue;
                }

                var line = MusicalLine.GetNewMusicalLine(MusicalLineType.Melodic, this.MusicalBlock);
                line.FirstStatus.Instrument = new MusicalInstrument((MidiMelodicInstrument)tg.InstrumentNumber);
                //// track.FirstStatus.Channel = this.Channel;
                //// track.FirstStatus.GChannel = new GeneralChannel(InstrumentGenus.Melodical, tg.Instrument, this.Channel);
                line.Purpose = this.Purpose;
                //// track.Purpose = this.Purpose; //// 2019/01
                this.MusicalLines.Add(line);

                voiceObjects.ForAll(musicalObject => {
                    if (musicalObject is MusicalStrike tone)
                    {
                        line.AddMusicalTone(tone);
                    }
                });
            }
        }

        /// <summary>
        /// Lays the objects to voice tracks.
        /// </summary>
        public void LayObjectsToVoiceTracks() {
            //// MusicalTones group by staff and voice
            var trackGroups = (from mt in this.MusicalObjects
                               select new { mt.Staff, mt.Voice }).Distinct().ToList();
            this.MusicalLines = new List<MusicalLine>();
            foreach (var tg in trackGroups) {
                var tg1 = tg;
                var voiceObjects = (from mt in this.MusicalObjects
                                    where mt.Staff == tg1.Staff && mt.Voice == tg1.Voice
                                    select mt).ToList();
                if (!voiceObjects.Any()) {
                    continue;
                }

                var line = MusicalLine.GetNewMusicalLine(0, this.MusicalBlock);
                line.FirstStatus.Instrument = this.Instrument;
                line.MainVoice.Channel = this.Channel;

                //// track.Status.GChannel = new GeneralChannel(InstrumentGenus.Melodical, this.Instrument, this.Channel);
                line.Purpose = this.Purpose;
                //// track.Purpose = this.Purpose; //// 2019/01
                this.MusicalLines.Add(line);
                //// Values of BitFrom must be determined here !!!
                var bitFrom = 0;
                var lastBarNumber = -1;
                voiceObjects.ForAll(musicalObject => {
                    if (musicalObject.BarNumber != lastBarNumber) {
                        bitFrom = 0;
                        lastBarNumber = musicalObject.BarNumber;
                    }

                    if (musicalObject is MusicalShift shift)
                    {
                        bitFrom = bitFrom + shift.Value;
                    }

                    // ReSharper disable once InvertIf
                    if (musicalObject is MusicalStrike tone && bitFrom >= 0)
                    {
                        tone.BitFrom = (byte)bitFrom;
                        line.AddMusicalTone(tone);
                        bitFrom = bitFrom + tone.Duration;
                    }
                });
                //// track.MusicalOctave = track.MusicalTones. 
            }
        }
        #endregion 

        #region Private methods
        /// <summary>
        /// Gets new musical track.
        /// </summary>
        /// <param name="givenBlock">The given block.</param>
        /// <returns>
        /// Returns value.
        /// </returns>
        private static MusicalPart GetNewMusicalPart(MusicalBlock givenBlock) {
            var part = new MusicalPart(givenBlock);
            return part;
        }
        #endregion
    }
}